<script lang='ts' setup>
import * as handPoseDetection from '@tensorflow-models/hand-pose-detection';
import { Keypoint } from '@tensorflow-models/hand-pose-detection';
import { cloneDeep, find, head, isEqual, last } from 'lodash';
import { atan2 } from 'mathjs';
import { Random } from 'mockjs';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import Stats from 'three/addons/libs/stats.module.js';
import { onMounted, ref, watch } from 'vue';
import { link_map } from './finger';

const threeDom = ref();

const props = defineProps({ handsData: Array });

const hands = ref();

// 容器大小
const width = ref(document.body.clientWidth);
const height = ref(document.body.clientHeight);

const planesFromMesh = (vertices: THREE.Vector3[], indices: string | any[]) => {
  const n = indices.length / 3,
    result = new Array(n);

  for (let i = 0, j = 0; i < n; ++i, j += 3) {

    const a = vertices[indices[j]],
      b = vertices[indices[j + 1]],
      c = vertices[indices[j + 2]];

    result[i] = new THREE.Plane().
      setFromCoplanarPoints(a, b, c);

  }

  return result;

};

const createPlanes = (n: number) => {
  const result = new Array(n);

  for (let i = 0; i !== n; ++i) {
    result[i] = new THREE.Plane();
  }
  return result;
};

const assignTransformedPlanes = (
  planesOut: {
    copy: (arg0: any) => {
      (): any;
      new(): any;
      applyMatrix4: {
        (arg0: any): void;
        new(): any;
      };
    };
  }[],
  planesIn: string | any[],
  matrix: THREE.Matrix4
) => {
  for (let i = 0, n = planesIn.length; i !== n; ++i) {
    planesOut[i].copy(planesIn[i]).applyMatrix4(matrix);
  }
};

const cylindricalPlanes = (n: number, innerRadius: number) => {
  const result = createPlanes(n);

  for (let i = 0; i !== n; ++i) {

    const plane = result[i],
      angle = i * Math.PI * 2 / n;

    plane.normal.set(
      Math.cos(angle), 0, Math.sin(angle));

    plane.constant = innerRadius;

  }
  return result;
};

const planeToMatrix = (function () {
  const xAxis = new THREE.Vector3();
  const yAxis = new THREE.Vector3();
  const trans = new THREE.Vector3();

  return function planeToMatrix(plane: { normal: any; coplanarPoint: (arg0: THREE.Vector3) => void; }) {
    const zAxis = plane.normal;
    const matrix = new THREE.Matrix4();

    if (Math.abs(zAxis.x) > Math.abs(zAxis.z)) {

      yAxis.set(- zAxis.y, zAxis.x, 0);

    } else {

      yAxis.set(0, - zAxis.z, zAxis.y);

    }

    xAxis.crossVectors(yAxis.normalize(), zAxis);
    plane.coplanarPoint(trans);
    return matrix.set(
      xAxis.x, yAxis.x, zAxis.x, trans.x,
      xAxis.y, yAxis.y, zAxis.y, trans.y,
      xAxis.z, yAxis.z, zAxis.z, trans.z,
      0, 0, 0, 1);
  };

})();

const Vertices = [
  new THREE.Vector3(+ 1, 0, + Math.SQRT1_2),
  new THREE.Vector3(- 1, 0, + Math.SQRT1_2),
  new THREE.Vector3(0, + 1, - Math.SQRT1_2),
  new THREE.Vector3(0, - 1, - Math.SQRT1_2)
];
const Indices = [0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2];
const Planes = planesFromMesh(Vertices, Indices);
const PlaneMatrices = Planes.map(planeToMatrix);
const GlobalClippingPlanes = cylindricalPlanes(5, 2.5);
const Empty = Object.freeze([]) as any;

let camera: THREE.Camera;
let scene: THREE.Scene;
let renderer: THREE.WebGLRenderer;
// let startTime: number;
let stats: Stats;
let object: THREE.Object3D<THREE.Object3DEventMap>;
let clipMaterial: THREE.MeshPhongMaterial | undefined;
let volumeVisualization: THREE.Object3D<THREE.Object3DEventMap>;
let globalClippingPlanes: any[];

watch(() => props?.handsData, handsData => {
  hands.value = handsData;
});

onMounted(() => {
  // width.value = width.value / 1.2;
  // height.value -= 80;

  camera = new THREE.PerspectiveCamera(36, width.value / height.value, 0.25, 16);
  camera.position.set(0, 1.5, 3);
  scene = new THREE.Scene();
  scene.add(new THREE.AmbientLight(0xffffff));

  const spotLight = new THREE.SpotLight(0xffffff, 60);

  spotLight.angle = Math.PI / 5;
  spotLight.penumbra = 0.2;
  spotLight.position.set(2, 3, 3);
  spotLight.castShadow = true;
  spotLight.shadow.camera.near = 3;
  spotLight.shadow.camera.far = 10;
  spotLight.shadow.mapSize.width = 1024;
  spotLight.shadow.mapSize.height = 1024;
  scene.add(spotLight);

  const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
  dirLight.position.set(0, 2, 0);
  dirLight.castShadow = true;
  dirLight.shadow.camera.near = 1;
  dirLight.shadow.camera.far = 10;
  dirLight.shadow.camera.right = 1;
  dirLight.shadow.camera.left = - 1;
  dirLight.shadow.camera.top = 1;
  dirLight.shadow.camera.bottom = - 1;
  dirLight.shadow.mapSize.width = 1024;
  dirLight.shadow.mapSize.height = 1024;
  scene.add(dirLight);

  clipMaterial = new THREE.MeshPhongMaterial({
    color: Random.color(),
    shininess: 100,
    side: THREE.DoubleSide,
    clippingPlanes: createPlanes(Planes.length),
    clipShadows: true
  });
  object = new THREE.Group();

  const geometry = new THREE.BoxGeometry(0.18, 0.18, 0.18);

  for (let z = - 2; z <= 2; ++z) {
    for (let y = - 2; y <= 2; ++y) {
      for (let x = - 2; x <= 2; ++x) {
        const mesh = new THREE.Mesh(geometry, clipMaterial);
        mesh.position.set(x / 5, y / 5, z / 5);
        mesh.castShadow = true;
        object.add(mesh);
      }
    }
  }
  scene.add(object);

  const planeGeometry = new THREE.PlaneGeometry(3, 3, 1, 1);
  const color = new THREE.Color();

  volumeVisualization = new THREE.Group();
  volumeVisualization.visible = false;

  for (let i = 0, n = Planes.length; i !== n; ++i) {
    const material = new THREE.MeshBasicMaterial({
      color: color.setHSL(i / n, 0.5, 0.5).getHex(),
      side: THREE.DoubleSide,
      opacity: 0.2,
      transparent: true,
      clippingPlanes: clipMaterial?.clippingPlanes?.
        filter((_, j) => j !== i)
    });

    const mesh = new THREE.Mesh(planeGeometry, material);
    mesh.matrixAutoUpdate = false;
    volumeVisualization.add(mesh);
  }
  scene.add(volumeVisualization);

  const ground = new THREE.Mesh(
    planeGeometry,
    new THREE.MeshPhongMaterial({
      color: 0xa0adaf, shininess: 10
    })
  );
  ground.rotation.x = - Math.PI / 2;
  ground.scale.multiplyScalar(3);
  ground.receiveShadow = true;
  scene.add(ground);

  const container = threeDom.value;
  renderer = new THREE.WebGLRenderer();
  renderer.shadowMap.enabled = true;
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width.value, height.value);
  window.addEventListener('resize', () => {
    (camera as any).aspect = width.value / height.value;
    (camera as any).updateProjectionMatrix();

    renderer.setSize(width.value, height.value);
  });
  container.appendChild(renderer.domElement);
  globalClippingPlanes = createPlanes(GlobalClippingPlanes.length);
  renderer.clippingPlanes = Empty;
  renderer.localClippingEnabled = true;
  stats = new Stats();
  container.appendChild(stats.dom);

  const controls = new OrbitControls(camera, renderer.domElement);
  controls.minDistance = 1;
  controls.maxDistance = 8;
  controls.target.set(0, 1, 0);
  controls.update();

  const gui = new GUI();
  const folder = gui.addFolder('Local Clipping');
  const props = {
    get 'Enabled'() {

      return renderer.localClippingEnabled;

    },
    set 'Enabled'(v) {

      renderer.localClippingEnabled = v;
      if (!v) volumeVisualization.visible = false;

    },

    get 'Shadows'() {

      return clipMaterial?.clipShadows;

    },
    set 'Shadows'(v) {

      clipMaterial && (clipMaterial.clipShadows = Boolean(v));

    },

    get 'Visualize'() {

      return volumeVisualization.visible;

    },
    set 'Visualize'(v) {

      if (renderer.localClippingEnabled)
        volumeVisualization.visible = v;

    }
  };

  folder.add(props, 'Enabled');
  folder.add(props, 'Shadows' as any);
  folder.add(props, 'Visualize').listen();

  gui.addFolder('Global Clipping').add({
    get 'Enabled'() {
      return !isEqual(renderer.clippingPlanes, Empty);
    },
    set 'Enabled'(v) {
      renderer.clippingPlanes = (v ? globalClippingPlanes : Empty) as any;
    }
  }, 'Enabled');

  const setObjectWorldMatrix = (
    object: {
      parent: any;
      matrix: {
        copy: (arg0: any) => { (): any; new(): any; invert: { (): void; new(): any; }; };
      };
      applyMatrix4: (arg0: any) => void;
    },
    matrix: THREE.Matrix4
  ) => {
    const parent = object.parent;
    scene.updateMatrixWorld();
    object.matrix.copy(parent.matrixWorld).invert();
    object.applyMatrix4(matrix);
  }

  const transform = new THREE.Matrix4();
  const tmpMatrix = new THREE.Matrix4();

  const animate = () => {
    requestAnimationFrame(animate);

    const point = (cloneDeep(hands?.value) ?? []) as handPoseDetection.Hand[];
    const wrist = find(point[0]?.keypoints3D, i => i.name === head(head(link_map)));
    const x = wrist?.x ?? 0;
    const y = wrist?.y ?? 0;
    const z = wrist?.z ?? 0;
    const thumb_tip = find(point[0]?.keypoints3D, i => i.name === last(link_map[1]));
    const index_finger_tip = find(point[0]?.keypoints3D, i => i.name === last(link_map[2]));
    const pinky_finger_tip = find(point[0]?.keypoints3D, i => i.name === last(last(link_map)));
    const angle = (point1?: Keypoint, point2?: Keypoint) => {
      const { x: x1 = 0, y: y1 = 0 } = point1 ?? {};
      const { x: x2 = 0, y: y2 = 0 } = point2 ?? {};

      return atan2(y2 - y1, x2 - x1) * (180 / Math.PI);
    };
    const distance = (point1?: Keypoint, point2?: Keypoint) => {
      const { x: x1 = 0, y: y1 = 0, z: z1 = 0 } = point1 ?? {};
      const { x: x2 = 0, y: y2 = 0, z: z2 = 0 } = point2 ?? {};

      return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1)) * 100;
    };
    const time = distance(thumb_tip, index_finger_tip);

    object.position.x = x * 15;
    object.position.y = y + 1;
    object.position.z = z * 15;
    // object.rotation.x = time * 0.5;
    object.rotation.y = angle(thumb_tip, pinky_finger_tip);
    object.updateMatrix();
    transform.copy(object.matrix);

    const bouncy = Math.cos(time * 0.5) * 0.5 + 0.7;
    transform.multiply(tmpMatrix.makeScale(bouncy, bouncy, bouncy));
    assignTransformedPlanes((clipMaterial as any)?.clippingPlanes, Planes, transform);

    const planeMeshes = volumeVisualization.children as [];

    for (let i = 0, n = planeMeshes.length; i !== n; ++i) {
      tmpMatrix.multiplyMatrices(transform, PlaneMatrices[i]);
      setObjectWorldMatrix(planeMeshes[i], tmpMatrix);
    }
    transform.makeRotationY(time * 0.1);
    assignTransformedPlanes(globalClippingPlanes, GlobalClippingPlanes, transform);
    stats.begin();
    renderer.render(scene, camera);
    stats.end();
  }
  animate();
});
</script>

<template>
  <div class='three' ref='threeDom'></div>
</template>

<style lang='less' src='./style.less' scoped />